package za.org.jcicada.comms.impl.udp;

import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import za.org.jcicada.comms.IAcceptListener;
import za.org.jcicada.comms.exceptions.CommsException;
import za.org.jcicada.comms.impl.AcceptorBase;
import za.org.jcicada.comms.impl.tcp.TcpAcceptor;
import za.org.jcicada.comms.impl.tcp.TcpSelector;

public class UdpAcceptor extends AcceptorBase {

	private static final Log logger = LogFactory.getLog(UdpAcceptor.class);
	public static final String UDP = "udp";
	private DatagramSocket socket;
	private Map<SocketAddress, UdpConnection> clientConnections = new HashMap<SocketAddress, UdpConnection>();
	
	public UdpAcceptor(IAcceptListener acceptListener, URI baseLocationURI) throws IOException {
		super(acceptListener);
		if (baseLocationURI.getHost().equals(TcpAcceptor.ANY_ADDR)) {
			initialize(new InetSocketAddress(baseLocationURI.getPort()));
		} else {
			initialize(new InetSocketAddress(baseLocationURI.getHost(), baseLocationURI.getPort()));
		}
	}
	
	public UdpAcceptor(IAcceptListener acceptListener, int port) throws IOException {
		this(acceptListener, new InetSocketAddress(port));
	}
	
	public UdpAcceptor(IAcceptListener acceptListener, String host, int port) throws IOException {
		this(acceptListener, new InetSocketAddress(host, port));
	}
	
	public UdpAcceptor(IAcceptListener acceptListener, InetSocketAddress address) throws IOException {
		super(acceptListener);
		initialize(address);
	}

	/**
	 * Initialize the server socket for listening on the specified address and
	 * set it up as a reader in the TcpSelector.
	 * 
	 * @param address
	 *            the address to listen on
	 * @throws IOException
	 */
	private void initialize(InetSocketAddress address) throws IOException {
		final DatagramChannel channel = DatagramChannel.open();
		channel.configureBlocking(false);
		socket = channel.socket();
		socket.setReuseAddress(true);
		socket.bind(address);
		TcpSelector tcpSelector = TcpSelector.getInstance();
		tcpSelector.addReader(channel, new TcpSelector.ICallback() {

			/**
			 * On receipt of packets make sure that there is a local connection
			 * and pass it the data to process
			 */
			@Override
			public int read(ByteBuffer dst) throws IOException {
				dst.clear();
				SocketAddress address = channel.receive(dst);
				accept(address).receiveBuffer(dst);
				return 0;	// just don't return -1 because that would close the connection
			}

			@Override
			public int write(ByteBuffer src) throws IOException {
				return 0;
			}

			@Override
			public void close() throws IOException {
			}
			
			@Override
			public String toString() {
				return UdpAcceptor.this.toString();
			}
			
		});
	}

	/**
	 * Shut down the acceptor. Part of the IAcceptor interface.
	 * 
	 * @throws CommsException
	 *             If shutting down the acceptor fails.
	 */
	@Override
	public void close() throws IOException {
		logger.info(toString() + " closing");
		socket.close();
		super.close();
		logger.info(super.toString() + " has closed");	// super.toString because getLocalSocketAddress fails after close
	}
	
	@Override
	public boolean isClosed() {
		return socket.isClosed();
	}

	/**
	 * Determine the actual port the acceptor is listening on (used if 0 is
	 * given for the port number).
	 */
	public int getLocalPort() {
		return socket.getLocalPort();
	}

	@Override
	public String getProtocol() {
		return UDP;
	}

	@Override
	public URI getBaseLocationURI() {
		if (socket.getLocalAddress().isAnyLocalAddress()) {
			try {
				return URI.create(String.format("%s://%s:%d", getProtocol(), InetAddress.getLocalHost().getHostAddress(), getLocalPort()));
			} catch (UnknownHostException ex) {
				logger.fatal("Error trying to determine host address.", ex);
			}
			return null;
		} else {
			return URI.create(String.format("%s:/%s", getProtocol(), socket.getLocalSocketAddress()));
		}
	}
	
	UdpConnection accept(SocketAddress address) throws IOException {
		UdpConnection client = clientConnections.get(address);
		if (client == null) {
			logger.info(String.format("Client connected to %s from %s", this, address));
			UdpConnector connector = new UdpConnector(address, socket.getLocalSocketAddress());
			client = (UdpConnection) connector.connect();
			addQueue(client);
			clientConnections.put(address, client);
		}
		return client;
	}
	
	@Override
	public String toString() {
		return super.toString() + ":" + socket.getLocalSocketAddress();
	}

}
